package no.sachmuld.methodcombination.api.reflection.component;

import no.sachmuld.methodcombination.api.*;
import no.sachmuld.methodcombination.impl.MultiPostcondition;
import no.sachmuld.methodcombination.impl.MultiPrecondition;
import org.apache.log4j.Logger;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
 */
public class ContractCheckingMethodCombination<B, R> extends DelegatingMethodCombination<B, R> {

    private static Logger logger = Logger.getLogger(ContractCheckingMethodCombination.class);

    private MethodCombination<B, R> targetMethodCombination;

    public ContractCheckingMethodCombination() {
    }

    public ContractCheckingMethodCombination(MethodCombination<B, R> targetMethodCombination) {
        this.targetMethodCombination = targetMethodCombination;
    }

    public void setTargetMethodCombination(MethodCombination<B, R> targetMethodCombination) {
        this.targetMethodCombination = targetMethodCombination;
    }

    @Override
    public ContractCheckingMethodCombination<B, R> build(final Class behaviourClass, Method targetMethod) {

        final BehaviourMethod preconditionMethod = new BehaviourMethod.Builder(behaviourClass, targetMethod, false).prefix("before").extraParameterTypes(Context.class).returnType(Condition.class).build();
        final BehaviourMethod postconditionMethod;
        final boolean voidResult = targetMethod.getReturnType() == Void.TYPE;
        if (voidResult) {
            postconditionMethod = new BehaviourMethod.Builder(behaviourClass, targetMethod, false).prefix("after").extraParameterTypes(Context.class).returnType(Condition.class).build();
        } else {
            postconditionMethod = new BehaviourMethod.Builder(behaviourClass, targetMethod, false).prefix("after").extraParameterTypes(targetMethod.getReturnType(), Context.class).returnType(Condition.class).build();
        }

        validatedAgainst(preconditionMethod, postconditionMethod);

        buildChildren(behaviourClass, targetMethod, targetMethodCombination);

        executedBy(new AbstractMethodCombination<B, R>() {
            @Override
            public R execute(Object targetObject, BehaviourChain<? extends B> behaviourChain, final Object... parameters) {

                Map<Object, Context> contextMap = new HashMap<>();

                if (preconditionMethod.exists()) {
                    List<Condition> preconditions = new ArrayList<>();
                    for (Object behaviour : behaviourChain.bottomUp()) {
                        if (behaviourClass.isAssignableFrom(behaviour.getClass())) {
                            Context context = new Context();
                            contextMap.put(behaviour, context);
                            preconditions.add((Condition) preconditionMethod.invoke(behaviour, parameters, context));
                        }
                    }
                    MultiPrecondition multiPrecondition = new MultiPrecondition(preconditions);
                    logger.info(multiPrecondition);
                    assert multiPrecondition.getValue() : multiPrecondition;
                }

                R result = targetMethodCombination.execute(targetObject, behaviourChain, parameters);

                if (postconditionMethod.exists()) {
                    List<Condition> postconditions = new ArrayList<>();
                    for (Object behaviour : behaviourChain.bottomUp()) {
                        if (behaviourClass.isAssignableFrom(behaviour.getClass())) {
                            Context context = contextMap.get(behaviour);
                            if (voidResult) {
                                postconditions.add((Condition) postconditionMethod.invoke(behaviour, parameters, context));
                            } else {
                                postconditions.add((Condition) postconditionMethod.invoke(behaviour, parameters, result, context));
                            }
                        }
                    }
                    MultiPostcondition multiPostcondition = new MultiPostcondition(postconditions);
                    logger.info(multiPostcondition);
                    assert multiPostcondition.getValue() : multiPostcondition;
                }

                return result;

            }
        });

        return this;

    }

}
